package chapter2_nio.str;

import java.net.InetSocketAddress;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.SocketChannel;
import java.util.Iterator;
import java.util.Set;

public class Client {
    public static void main(String[] args) throws Exception {
        Client client=new Client("127.0.0.1",9998);
        client.communicate();
    }

    private Selector selector;
    /** 创建selector选择器对象，同时创建一个SocketChannel对象(用于连接服务端)。 然后将SocketChannel channel注册到选择器上，
     *  最后调用SocketChannel channel的connect()方法连接服务端。
     */
    protected  Client(String serverIP, int port)throws  Exception{//√ √ √ √ √ √ √ √ √ √ √ √ √ √ √ √ √ √ √ √ √ √ √ √ √ √ √ √ √ √ √ √
        //用Selector.open()可以获得一个新的选择器实例
        selector=Selector.open();
        SocketChannel channel=SocketChannel.open();//获取ServerSocketChannel实例
        channel.configureBlocking(false);
        //该函数用于向Selector注册SocketChannel，并指定此SocketChannel作用是和服务端连接
        channel.register(selector, SelectionKey.OP_CONNECT);
        //通过此SocketChannel连接服务端
        channel.connect(new InetSocketAddress(serverIP,port));//1.
    }


    public void closeChannel(SelectionKey selectionKey)throws Exception{//√ √ √ √ √ √ √ √ √ √ √ √ √ √ √ √ √ √ √ √ √ √ √ √ √ √ √ √ √ √ √ √
        selectionKey.channel().close();
        //关闭这个selectionKey对应的选择器Selector(会断开与该选择器相关的所有通道连接)
        selectionKey.selector().close();
    }

    /**获取selector中的所有selectionKey。 并判断此selectionKey是否是可连接的(是可连接的就连接，否则就进行数据的读写操作)。
     *    如果是可连接的就检查是否已经连接到服务端如果连接上了，就将该channel重新注册到选择器上(之前此channel用于连接服务端，以后此channel将用于读写数据)。
     *    如果不是可连接的就调用session方法进行数据的读写操作
     * 同时从获取的所有selectionKey中移除已经处理过的selectionKey(剩余的selectionKey是未处理的)
     *    */
    public void communicate()throws Exception{//√ √ √ √ √ √ √ √ √ √ √ √ √ √ √ √ √ √ √ √ √ √ √ √ √ √ √ √ √ √ √ √
        while(selector.isOpen()){
            //用选择器进行阻塞式等待，直到有某个SocketChannel通道准备好读操作
            // 或写操作或者准备和Server进行连接操作(或设置的其他操作触发)才会继续往下运行
            selector.select();
            //获取selector中的所有selectionKey组成的集合，并处理集合中每个selectionKey对应的SocketChannel的事件，
            // 处理完一个就将其对应的selectionKey从Set集合中移除
            Set<SelectionKey> selectionKeySet=selector.selectedKeys();
            Iterator<SelectionKey> iterator=selectionKeySet.iterator();
            while(iterator.hasNext()){
                SelectionKey selectionKey=iterator.next();
                if(selectionKey.isConnectable()){
                    SocketChannel channel=(SocketChannel) selectionKey.channel();
                    //该代码片段是用于等待SocketChannel完成连接的。(如果这个SocketChannel没连接完成，就继续循环检查，直到完成连接再继续向下执行)
                    //(SocketChannel是一个非阻塞模式的网络I/O模型，因此在进行连接时，可能不会立即完成)
                    while(!channel.finishConnect()){//
                    }
                    System.out.println("客户端连接成功，可以和服务端进行数据传输...");
                    //连接成功后需要将此SocketChannel channel重新注册到selector选择器上(因为此通道下一次就要用于数据传输而不是连接的作用)
                    registerOnConnected(channel,selector);//2.
                }else{//如果此selectionKey不是和服务端进行连接，则和服务端进行数据传输
                    session(selectionKey);//3.
                }//从获取的所有selectionKey中移除已经处理过的selectionKey
                iterator.remove();//(剩余的selectionKey是未处理的)
            }
            //System.out.println("客户端的操作完毕");
        }
    }


    public void session(SelectionKey key)throws Exception{
        SocketChannel channel=(SocketChannel) key.channel();
        if(key.isWritable()){
            SenderUtil sender=(SenderUtil) key.attachment();
            if(sender.sendFully(channel)){//3.发送数据"how are you?"
                System.out.println("向服务端发送数据：" + sender.getObject());
                if(sender.getStepIndex() == 0){//4.创建一个ReceiverUtil对象作为attachment
                    //用于接收服务端返回的数据"i`m fine ,thank you!"(ReceiverUtil receiver的stepIndex为1)
                    ReceiverUtil receiver=new ReceiverUtil(sender.getStepIndex()+1, String.class);
                    channel.register(key.selector(), SelectionKey.OP_READ, receiver);
                }


                //当sender的stepIndex==3满足时，证明客户端的四次操作都完毕
                // (1.发送how are you, 2.接收 i`m fine  和 3.接收i`m fine too。 4.发送i`m  fine too)
                //发送how are you时 sender.stepIndex=0。 接收i·m  fine  thank you时receiver.stepIndex=0+1为1
                //当接收and  you时receiver.stepIndex=1+1为2, 当发送i`m fine  too时 sender的stepIndex=receiver.stepIndex2+1
                if(sender.getStepIndex() == 3){//如果发送数据"I`m  fine  too"完毕就关闭和Server对应的channel
                    closeChannel(key);
                }
            }
        }else if(key.isReadable()){//如果SelectionKey是可读状态
            // (就用recvFully方法接收数据，然后接收数据或发送数据<然后将此channel绑定一个接收对象或者发送对象>)
            ReceiverUtil receiver=(ReceiverUtil) key.attachment();
            //注意：recvFully()方法可能会执行多次，(因为Client类的communicate()方法中循环调用session方法
            // (session方法又调用recvFully方法))所以可能会多次执行recvFully()方法读取数据长度到ReceiverUtil的countBuffer，
            //多次执行读取对象数据到ReceiverUtil的buffer直到数据读完，recvFully()方法返回true，
            //就执行下边if分支(如果第一次接收数据，就给SelectionKey绑定一个attachment对象SenderUtil(其对应的stepIndex为1))
            if(receiver.recvFully(channel)){//5.用key对应的ReceiverUtil的recvFully方法接收数据长度和数据"i`m fine ,thank you!"
                //8.第二次执行recvFully()方法读取数据"and you?"
                String s=(String)receiver.getObject();
                System.out.println("服务端返回的数据：" + s);
                if(receiver.getStepIndex() == 1){//6.如果第一次接收服务端返回的数据就将SelectionKey绑定一个
                    // attachment对象ReceiverUtil(其对应的stepIndex为2)用于接收服务端返回的"and you?"
                    key.attach( new ReceiverUtil(receiver.getStepIndex() + 1 , String.class)  );
                }
                if(receiver.getStepIndex() == 2){//7.如果是第二次接收服务端返回数据，就将SelectionKey绑定
                    // 一个attachment对象SenderUtil(其对应的stepIndex为3)，用于给服务端发送数据"I`m  fine  too."
                    SenderUtil sender=new SenderUtil(receiver.getStepIndex() +1, "I`m  fine  too.");
                    channel.register( key.selector(), SelectionKey.OP_WRITE, sender);
                }
            }
        }

    }

    /**连接成功后，就立即创建一个SenderUtil，用于向selector中注册的channel发送数据(channel的状态为OP_WRITE)*/
    public void registerOnConnected(SocketChannel channel,Selector selector)throws Exception{//2.
        System.out.println("模拟将SocketChannel:"+channel+"注册到Selector选择器上");
        SenderUtil sender=new SenderUtil(0,"how are you?");
        channel.register(selector, SelectionKey.OP_WRITE, sender);
    }


}